package trb.complexshield.benchmarking;

import java.io.File;
import java.io.FileInputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import no.hrp.hvrc.property.PropertyOwnerFactory;
import no.hrp.hvrc.property.SimpleIO;
import no.hrp.hvrc.rad.calc.intersect.Segment;
import no.hrp.hvrc.utils.j3d.BBox;
import no.hrp.hvrc.utils.j3d.Mat4;
import no.hrp.hvrc.utils.j3d.Vec3;
import no.hrp.hvrc.utils.util.ArrayUtils;
import no.hrp.hvrc.utils.util.IOUtils;
import no.hrp.hvrc.utils.xml.XMLElement;
import no.hrp.hvrc.utils.xml.XMLOut;
import trb.complexshield.ComplexShield;
import trb.complexshield.ComplexShieldOutput;
import trb.complexshield.bool.model.AABBCreator;
import trb.complexshield.voxel.VoxelFromBool;

public class BenchmarkAll {

    //public static final int WARMUP_COUNT = 1;
    //public static final int PERFORMANCE_COUNT = 2;
    public static final int SOURCE_COUNT = 10;
    public static final double BBOX_GROW_FACTOR = 1.5;
    private static String BOOLEAN_TEXT = "Bool";
    private static String VOXEL_TEXT = "Voxel";
    private static String BRUTE_MESH_TEXT = "Brute";
    private static String BVH_MESH_TEXT = "BVH";
    private static String BVH_ADJUSTED_RADIUS_MESH_TEXT = "BVH_ADJUSTED_RADIUS";

    public static final String BOX_TEXT = "Box";
    public static final String SPHERE_TEXT = "Sphere";
    public static final String TUBE_TEXT = "Tube";
    public static final String TUB_TEXT = "Tub";
    public static final String ROOMS_TEXT = "Rooms";
    public static final String[] SHAPE_NAMES = {BOX_TEXT, SPHERE_TEXT, TUB_TEXT, TUBE_TEXT, ROOMS_TEXT};


    public static void main(String[] args) {
        Settings settings = new Settings(true, true, true, 1, 2, new int[] {32});

//        findTimerResolution();
        List<BenchmarkOutput> outputs = new ArrayList<>();
        outputs.addAll(benchmarkTubes(settings));
        outputs.addAll(benchmarkRooms(settings));
        outputs.addAll(benchmarkSpheres(settings));
        outputs.addAll(benchmarkTubs(settings));
        outputs.addAll(benchmarkBoxes(settings));
        String report = createReport(outputs);
        System.out.println(report);

        //System.out.println(benchmarkOutputList.toString());

//        outputs.clear();
//        outputs.addAll(benchmarkTubs());
//        report = createReport(outputs);
//        System.out.println(report);
    }

    public static void writeStringToFile(File file, String string) {
        try {
            PrintWriter out = new PrintWriter(file);
            out.print(string);
            out.close();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    public static String readStringFromFile(File file) {
        String string = "";
        try {
            FileInputStream in = new FileInputStream(file);
            string = new String(IOUtils.readFully(in));
            in.close();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return string;
    }

    public static String benchmarkOutputListToString(List<BenchmarkOutput> outputs) {
        XMLElement benchmarkOutputList = XMLElement.create("BenchmarkOutputs");
        XMLOut xmlOut = new XMLOut();
        for (BenchmarkOutput output : outputs) {
            XMLElement benchmarkOutput = SimpleIO.getOwnerAsElement("BenchmarkOutput", output, xmlOut);
            benchmarkOutputList.children.add(benchmarkOutput);
        }
        return benchmarkOutputList.toString();
    }

    public static List<BenchmarkOutput> benchmarkOutputListFromString(String string) {
        List<BenchmarkOutput> outputs = new ArrayList<>();
        try {
            XMLElement  fromContent = new XMLElement(string);
            for (XMLElement child : fromContent.child(0).children) {
                BenchmarkOutput benchmarkOutput = PropertyOwnerFactory.create(BenchmarkOutput.class);
                SimpleIO.readOwner(benchmarkOutput, child);
                outputs.add(benchmarkOutput);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return outputs;
    }

    private static void findTimerResolution() {
        long[] times = new long[100];
        for (int i=0; i<100; i++) {
            times[i] = System.nanoTime();
        }
        for (int i=1; i<100; i++) {
            long dif = times[i] - times[i - 1];
            if (dif != 0) {
                System.out.println(dif);
            }
        }
    }

    public static List<BenchmarkOutput> benchmarkTubes(Settings settings) {
        return benchmarkTubes(settings, new int[] {3, 4, 8, 16, 32, 64, 128});
    }

    public static List<BenchmarkOutput> benchmarkTubes(Settings settings, int[] divisions) {
        BenchmarkTube tube = new BenchmarkTube(0.5f, 1f, 0f, 2f, new Mat4());
        List<Segment> segments = createSegments(getGrownAABB(BenchmarkTube.createBoolModel(tube)));
        float[] correctTotals = getTotals(BenchmarkTube.createBool(tube), segments);

        AtomicInteger triangleCount = new AtomicInteger(0);
        List<BenchmarkOutput> outputs = new ArrayList<>();
        if (settings.runBool) {
            outputs.add(doBenchmark(TUBE_TEXT, BOOLEAN_TEXT
                    , () -> BenchmarkTube.createBool(tube)
                    , segments, correctTotals, settings.warmeupCount, settings.performanceCount));
        }
        if (settings.runMesh) {
            for (int division : divisions) {
                BenchmarkOutput output = doBenchmark(TUBE_TEXT, BRUTE_MESH_TEXT/* + " " + division*/
                        , () -> BenchmarkTube.createClosedMesh(tube, division, triangleCount)
                        , segments, correctTotals, settings.warmeupCount, settings.performanceCount);
                output.triangleCount.set(triangleCount.get());
                outputs.add(output);
            }
            for (int division : divisions) {
                BenchmarkOutput output = doBenchmark(TUBE_TEXT, BVH_MESH_TEXT/* + " " + division*/
                        , () -> BenchmarkTube.createBvhMesh(tube, division, triangleCount)
                        , segments, correctTotals, settings.warmeupCount, settings.performanceCount);
                output.triangleCount.set(triangleCount.get());
                outputs.add(output);
            }
        }
        if (settings.runVoxel) {
            for (int dim : settings.voxelDims) {
                BenchmarkOutput output = doBenchmark(TUBE_TEXT, VOXEL_TEXT + " " + dim
                        , () -> BenchmarkTube.createVoxel(tube, dim)
                        , segments, correctTotals, settings.warmeupCount, settings.performanceCount);
                output.voxelDim.set(dim);
                output.voxelDims.set(VoxelFromBool.getDims(BenchmarkTube.createBoolModel(tube), dim));
                outputs.add(output);
            }
        }

        setCorrectSum(outputs, ArrayUtils.sum(correctTotals));
        return outputs;
    }

    public static List<BenchmarkOutput> benchmarkTubs(Settings settings) {
        Object boolModel = BenchmarkTub.createBoolModel();
        List<Segment> segments = createSegments(getGrownAABB(boolModel));
        float[] correctTotals = getTotals(BenchmarkTub.createBool(), segments);

        List<BenchmarkOutput> outputs = new ArrayList<>();
        if (settings.runBool) {
            outputs.add(doBenchmark(TUB_TEXT, BOOLEAN_TEXT
                    , () -> BenchmarkTub.createBool()
                    , segments, correctTotals, settings.warmeupCount, settings.performanceCount));
        }
        if (settings.runMesh) {
            outputs.add(doBenchmark(TUB_TEXT, BRUTE_MESH_TEXT
                    , () -> BenchmarkTub.createClosedMesh()
                    , segments, correctTotals, settings.warmeupCount, settings.performanceCount));
            outputs.add(doBenchmark(TUB_TEXT, BVH_MESH_TEXT
                    , () -> BenchmarkTub.createBvhMesh()
                    , segments, correctTotals, settings.warmeupCount, settings.performanceCount));
        }
        if (settings.runVoxel) {
            for (int dim : settings.voxelDims) {
                BenchmarkOutput output = doBenchmark(TUB_TEXT, VOXEL_TEXT + " " + dim
                        , () -> VoxelFromBool.createVoxel(boolModel, dim)
                        , segments, correctTotals, settings.warmeupCount, settings.performanceCount);
                output.voxelDim.set(dim);
                output.voxelDims.set(VoxelFromBool.getDims(boolModel, dim));
                outputs.add(output);
            }
        }

        setCorrectSum(outputs, ArrayUtils.sum(correctTotals));
        return outputs;
    }

    public static List<BenchmarkOutput> benchmarkRooms(Settings settings) {
        List<Segment> segments = createSegments(getGrownAABB(BenchmarkRooms.createBoolModel()));
        float[] correctTotals = getTotals(BenchmarkRooms.createBool(), segments);

        List<BenchmarkOutput> outputs = new ArrayList<>();
        if (settings.runBool) {
            outputs.add(doBenchmark(ROOMS_TEXT, BOOLEAN_TEXT
                    ,  () -> BenchmarkRooms.createBool()
                    , segments, correctTotals, settings.warmeupCount, settings.performanceCount));
        }
        if (settings.runMesh) {
            outputs.add(doBenchmark(ROOMS_TEXT, BRUTE_MESH_TEXT
                    , () -> BenchmarkRooms.createClosedMesh()
                    , segments, correctTotals, settings.warmeupCount, settings.performanceCount));
            outputs.add(doBenchmark(ROOMS_TEXT, BVH_MESH_TEXT
                    , () -> BenchmarkRooms.createBvhMesh()
                    , segments, correctTotals, settings.warmeupCount, settings.performanceCount));
        }
        if (settings.runVoxel) {
            for (int dim : settings.voxelDims) {
                BenchmarkOutput output = doBenchmark(ROOMS_TEXT, VOXEL_TEXT + " " + dim
                        , () -> VoxelFromBool.createVoxel(BenchmarkRooms.createBoolModel(), dim)
                        , segments, correctTotals, settings.warmeupCount, settings.performanceCount);
                output.voxelDim.set(dim);
                output.voxelDims.set(VoxelFromBool.getDims(BenchmarkRooms.createBoolModel(), dim));
                outputs.add(output);

            }
        }

        setCorrectSum(outputs, ArrayUtils.sum(correctTotals));
        return outputs;
    }

    public static List<BenchmarkOutput> benchmarkSpheres(Settings settings) {
        BenchmarkSphere sphere = new BenchmarkSphere(1, new Mat4());
        List<Segment> segments = createSegments(getGrownAABB(BenchmarkSphere.createBoolModel(sphere)));
        float[] correctTotals = getTotals(BenchmarkSphere.createBool(sphere), segments);
        int[] divisions = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        float[] adjustedRadius = {Float.POSITIVE_INFINITY
                ,1.4644718f
                ,1.124629f
                ,1.054199f
                ,1.0295405f
                ,1.0203209f
                ,1.0138838f
                ,1.0107903f
                ,1.0065908f
                ,1.0053711f
                ,1.0053713f
                ,1.0053787f
                ,1.0030429f
                ,1.0034192f
                ,1.0020447f
                ,1.001585f
                ,1.001581f
                ,1.002079f
                ,1.000366f};

        AtomicInteger triangleCount = new AtomicInteger(0);

        List<BenchmarkOutput> outputs = new ArrayList<>();
        if (settings.runBool) {
            outputs.add(doBenchmark(SPHERE_TEXT, BOOLEAN_TEXT
                    , () -> BenchmarkSphere.createBool(sphere)
                    , segments, correctTotals, settings.warmeupCount, settings.performanceCount));
        }
        if (settings.runMesh) {
            for (int division : divisions) {
                BenchmarkOutput output = doBenchmark(SPHERE_TEXT, BRUTE_MESH_TEXT/* + " " + division*/
                        , () -> BenchmarkSphere.createClosedMesh(sphere, division, triangleCount)
                        , segments, correctTotals, settings.warmeupCount, settings.performanceCount);
                output.triangleCount.set(triangleCount.get());
                outputs.add(output);
            }
            for (int division : divisions) {
                BenchmarkOutput output = doBenchmark(SPHERE_TEXT, BVH_MESH_TEXT/* + " " + division*/
                        , () -> BenchmarkSphere.createBvhMesh(sphere, division, triangleCount)
                        , segments, correctTotals, settings.warmeupCount, settings.performanceCount);
                output.triangleCount.set(triangleCount.get());
                outputs.add(output);
            }
            for (int division : divisions) {
                BenchmarkSphere adjustedSphere = new BenchmarkSphere(sphere.radius * adjustedRadius[division], sphere.mat4);
                BenchmarkOutput output = doBenchmark(SPHERE_TEXT, BVH_ADJUSTED_RADIUS_MESH_TEXT
                        , () -> BenchmarkSphere.createBvhMesh(adjustedSphere, division, triangleCount)
                        , segments, correctTotals, settings.warmeupCount, settings.performanceCount);
                output.triangleCount.set(triangleCount.get());
                outputs.add(output);
            }
        }
        if (settings.runVoxel) {
            for (int dim : settings.voxelDims) {
                BenchmarkOutput output = doBenchmark(SPHERE_TEXT, VOXEL_TEXT + " " + dim
                        , () -> BenchmarkSphere.createVoxel(sphere, dim)
                        , segments, correctTotals, settings.warmeupCount, settings.performanceCount);
                output.voxelDim.set(dim);
                output.voxelDims.set(VoxelFromBool.getDims(BenchmarkSphere.createBoolModel(sphere), dim));
                outputs.add(output);
            }
        }

        setCorrectSum(outputs, ArrayUtils.sum(correctTotals));
        return outputs;
    }

    public static List<BenchmarkOutput> benchmarkBoxes(Settings settings) {
        BenchmarkBox box = new BenchmarkBox(new Vec3(-25, -25, -25), new Vec3(25, 25, 25), new Mat4().rotateEulerDeg(20, 30, 40));
        List<Segment> segments = createSegments(getGrownAABB(BenchmarkBox.createBoolModel(box)));
        float[] correctTotals = getTotals(BenchmarkBox.createBool(box), segments);

        List<BenchmarkOutput> outputs = new ArrayList<>();
        if (settings.runBool) {
            outputs.add(doBenchmark(BOX_TEXT, BOOLEAN_TEXT
                    , () -> BenchmarkBox.createBool(box)
                    , segments, correctTotals, settings.warmeupCount, settings.performanceCount));
        }
        if (settings.runMesh) {
            outputs.add(doBenchmark(BOX_TEXT, BRUTE_MESH_TEXT
                    , () -> BenchmarkBox.createClosedMesh(box)
                    , segments, correctTotals, settings.warmeupCount, settings.performanceCount));
            outputs.add(doBenchmark(BOX_TEXT, BVH_MESH_TEXT
                    , () -> BenchmarkBox.createBvhMesh(box)
                    , segments, correctTotals, settings.warmeupCount, settings.performanceCount));
        }
        if (settings.runVoxel) {
            for (int dim : settings.voxelDims) {
                BenchmarkOutput output = doBenchmark(BOX_TEXT, VOXEL_TEXT + " " + dim
                        , () -> BenchmarkBox.createVoxel(box, dim)
                        //, () -> VoxelFromBool.createVoxel(BenchmarkRooms.createBoolModel(), dim)
                        , segments, correctTotals, settings.warmeupCount, settings.performanceCount);
                output.voxelDim.set(dim);
                output.voxelDims.set(VoxelFromBool.getDims(BenchmarkBox.createBoolModel(box), dim));
                outputs.add(output);
            }
        }

        setCorrectSum(outputs, ArrayUtils.sum(correctTotals));
        return outputs;
    }

    public static BBox getGrownAABB(Object boolModel) {
        BBox bbox = AABBCreator.createAABB(boolModel);
        return BBox.fromCenterAndSize(bbox.getCenter(), bbox.getSize().scale(BBOX_GROW_FACTOR));
    }

    public static List<Segment> createSegments(BBox bbox) {
        float[] measurePositions = SegmentsBenchmark.createMeasurePositions(bbox);
        float[] sourcePositions = SegmentsBenchmark.createSourcePositions(
                bbox.getLower(), bbox.getUpper(), SOURCE_COUNT);
        return SegmentsBenchmark.createSegments(sourcePositions, measurePositions);
    }

    private static void setCorrectSum(List<BenchmarkOutput> outputs, float correctSum) {
        for (BenchmarkOutput output : outputs) {
            output.correctSum.set(correctSum);
        }
    }

    private static BenchmarkOutput doBenchmark(String shapeName, String name
            , Supplier<ComplexShield> shieldCreator
            , List<Segment> segments, float[] correctTotals, int warmupCount, int performanceCount) {
        System.out.print("start benchmark " + name + " ... ");
        BenchmarkOutput benchmarkOutput = BenchmarkOutput.create(shapeName, name);
        benchmarkOutput.memoryUsedBeforeCreation.set(getMemoryUsed());
        long time0 = System.nanoTime();
        ComplexShield complexShield = shieldCreator.get();
        long time1 = System.nanoTime();
        benchmarkOutput.creationTimeNano.set(time1 - time0);
        benchmarkOutput.memoryUsedAfterCreation.set(getMemoryUsed());
        for (int i=0; i<warmupCount; i++) {
            benchmarkPerformance(complexShield, segments, BenchmarkOutput.create(shapeName, "dummy"));
        }
        for (int i=0; i<performanceCount; i++) {
            BenchmarkOutput temp = BenchmarkOutput.create(shapeName, "temp");
            benchmarkPerformance(complexShield, segments, temp);
            benchmarkOutput.timeNano.set(benchmarkOutput.timeNano.get() + temp.timeNano.get());
            benchmarkOutput.totalDistanceInsideShield.set(temp.totalDistanceInsideShield.get());
        }
        benchmarkOutput.timeNano.set(benchmarkOutput.timeNano.get() / performanceCount);
        benchmarkOutput.memoryUsedAfterPerformance.set(getMemoryUsed());
        benchmarkCorrectness(complexShield, segments, correctTotals, benchmarkOutput);
        System.out.println("done");
        return benchmarkOutput;
    }

    public static long getMemoryUsed() {
        Runtime runtime = Runtime.getRuntime();
        System.gc();
        return runtime.totalMemory() - runtime.freeMemory();
    }

    static void benchmarkPerformance(ComplexShield complexShield, List<Segment> segments, BenchmarkOutput out) {
        ComplexShieldOutput complexShieldOutput = new ComplexShieldOutput();
        long start = System.nanoTime();
        float totalDistanceInsideShield = 0f;
        for (Segment segment : segments) {
            complexShieldOutput.clear();
            complexShield.intersect(segment, complexShieldOutput);
            for (int i=0; i<complexShieldOutput.size; i+=2) {
                float tMin = complexShieldOutput.data[i];
                float tMax = complexShieldOutput.data[i+1];
                totalDistanceInsideShield += (tMax - tMin);
            }
        }
        long end = System.nanoTime();
        out.totalDistanceInsideShield.set(totalDistanceInsideShield);

        out.timeNano.set(end - start);
    }

    public static void benchmarkCorrectness(ComplexShield testShield, List<Segment> segments, float[] correctTotal, BenchmarkOutput out) {
        float totalDifferance = 0;
        float testTotal = 0;
        float[] testTotals = getTotals(testShield, segments);
        for (int i=0; i<testTotals.length; i++) {
            testTotal += testTotals[i];
            totalDifferance += Math.abs(correctTotal[i] - testTotals[i]);
        }
        out.totalDifferance.set(totalDifferance);
        out.testTotal.set(testTotal);
    }

    /**
     * Gets the total length inside the shields for each segment in segments.
     */
    public static float[] getTotals(ComplexShield shield, List<Segment> segments) {
        float[] totals = new float[segments.size()];
        for (int i=0; i<segments.size(); i++) {
            Segment segment = segments.get(i);
            ComplexShieldOutput out = new ComplexShieldOutput();
            shield.intersect(new Segment(segment.originRef(), segment.targetRef()), out);
            totals[i] = out.getTotal(segment);
        }
        return totals;
    }

    private static String createReport(List<BenchmarkOutput> benchmarkOutputs) {
        String DELIM = "\t";
        StringBuilder str = new StringBuilder();
        str.append("name").append(DELIM)
                .append("CreationTimeSec").append(DELIM)
                .append("timeSec").append(DELIM)
                //.append("totalDistanceInsideShield").append(DELIM)
                //.append("correctTotal").append(DELIM)
                //.append("testTotal").append(DELIM)
                .append("totalDifferance").append(DELIM)
                .append("totalError%").append(DELIM)
                .append("memoryUsedByCreation").append(DELIM)
                .append("memoryUsedByRunning").append(DELIM)
                .append("\n");
        for (BenchmarkOutput benchmarkOutput : benchmarkOutputs) {
            str.append(benchmarkOutput.name).append(DELIM)
                    .append(benchmarkOutput.creationTimeNano.get() / 1E9).append(DELIM)
                    .append(benchmarkOutput.timeNano.get() / 1E9).append(DELIM)
                    //.append(benchmarkOutput.totalDistanceInsideShield).append(DELIM)
                    //.append(benchmarkOutput.testTotal).append(DELIM)
                    .append(benchmarkOutput.totalDifferance).append(DELIM)
                    .append(benchmarkOutput.totalDifferance.get() * 100f / benchmarkOutput.correctSum.get()).append(DELIM)
                    //.append((benchmarkOutput.testTotal - benchmarkOutput.correctSum) * 100f / benchmarkOutput.correctSum).append(DELIM)
                    .append((benchmarkOutput.memoryUsedAfterCreation.get() - benchmarkOutput.memoryUsedBeforeCreation.get())).append(DELIM)
                    .append((benchmarkOutput.memoryUsedAfterPerformance.get() - benchmarkOutput.memoryUsedAfterCreation.get())).append(DELIM)
                    .append("\n")
            ;
        }

        str.append(createTimeReport(benchmarkOutputs, DELIM));

        String s = str.toString();
        s = s.replaceAll("\\.", ",");
        return s;
    }

    private static String createTimeReport(List<BenchmarkOutput> benchmarkOutputs, String DELIM) {
        StringBuilder str = new StringBuilder();
        for (BenchmarkOutput benchmarkOutput : benchmarkOutputs) {
            str.append(benchmarkOutput.name).append(DELIM);
        }
        str.append("\n");
        for (BenchmarkOutput benchmarkOutput : benchmarkOutputs) {
            str.append(benchmarkOutput.timeNano.get() / 1E9).append(DELIM);
        }
        str.append("\n");
        return str.toString();
    }


    public static class Settings {

        public int warmeupCount;
        public int performanceCount;
        private int[] voxelDims;
        public boolean runBool;
        public boolean runMesh;
        public boolean runVoxel;

        public Settings(boolean runBool, boolean runMesh, boolean runVoxel
                , int warmeupCount, int performanceCount, int[] voxelDims) {
            this.runBool = runBool;
            this.runMesh = runMesh;
            this.runVoxel = runVoxel;
            this.warmeupCount = warmeupCount;
            this.performanceCount = performanceCount;
            this.voxelDims = voxelDims;
        }
    }
}
